package com.childenglish.service;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class FileChunkService {

    private static final Logger logger = LoggerFactory.getLogger(FileChunkService.class);

    @Value("${file.upload-dir:uploads}")
    private String uploadDir;

    @Value("${file.chunk-dir:chunks}")
    private String chunkDir;

    @Value("${file.max-chunk-size:10485760}") // 10MB
    private long maxChunkSize;

    @Value("${file.max-file-size:1073741824}") // 1GB
    private long maxFileSize;

    @Autowired
    @org.springframework.context.annotation.Lazy
    private FileStorageService fileStorageService;

    @Autowired
    private com.childenglish.mapper.FileInfoMapper fileInfoMapper;

    // 存储上传进度
    private final Map<String, Set<Integer>> uploadProgress = new ConcurrentHashMap<>();

    /**
     * 检查分片状态
     */
    public Map<String, Object> checkChunkStatus(String fileMd5, String fileName,
                                                Long fileSize, Integer chunkSize, Integer totalChunks) {
        Map<String, Object> result = new HashMap<>();

        try {
            // 验证文件大小
            if (fileSize > maxFileSize) {
                throw new IOException("文件大小超过限制: " + (maxFileSize / 1024 / 1024) + "MB");
            }

            // 检查文件是否真实存在（同时检查数据库和文件系统）
            // 只有当数据库有记录 AND 文件系统也存在文件时，才认为文件已存在
            String finalFileName = getFinalFileName(fileMd5, fileName);
            Path finalFilePath = Paths.get(uploadDir, finalFileName);
            com.childenglish.entity.FileInfo existingFile = fileInfoMapper.selectByFileMd5(fileMd5);
            boolean fileExistsInFs = Files.exists(finalFilePath);
            
            if (existingFile != null && fileExistsInFs) {
                // 数据库有记录且文件系统也存在，说明文件真实存在
                // 检查文件类型是否匹配，如果不匹配则更新
                String expectedFileType = getFileTypeFromName(fileName);
                String currentFileType = existingFile.getFileType();
                
                if (currentFileType == null || currentFileType.isEmpty() || 
                    ("other".equals(currentFileType) && !"other".equals(expectedFileType))) {
                    // 如果当前类型是 null、空字符串或 'other'，而期望类型不是 'other'，则更新
                    try {
                        // 使用 updateFileTypeAndReviewStatus 更新文件类型，保持原有的 reviewStatus
                        Integer reviewStatus = existingFile.getReviewStatus() != null ? 
                            existingFile.getReviewStatus() : 0;
                        fileInfoMapper.updateFileTypeAndReviewStatus(
                            existingFile.getFileName(), expectedFileType, reviewStatus);
                        // 更新内存中的对象，以便后续使用
                        existingFile.setFileType(expectedFileType);
                        logger.info("更新文件类型: MD5={}, fileName={}, 从 {} 更新为 {}", 
                            fileMd5, existingFile.getFileName(), currentFileType, expectedFileType);
                    } catch (Exception e) {
                        logger.warn("更新文件类型失败: MD5={}, error={}", fileMd5, e.getMessage());
                        // 更新失败不影响返回结果
                    }
                }
                
                logger.info("检测到重复文件（数据库和文件系统都存在），MD5: {}, 原始文件名: {}, 数据库文件名: {}, 文件类型: {}", 
                    fileMd5, fileName, existingFile.getOriginalName(), existingFile.getFileType());
                result.put("status", "completed");
                result.put("fileName", existingFile.getFileName());
                result.put("originalName", existingFile.getOriginalName());
                result.put("message", "文件已存在，不能重复上传。已存在的文件: " + existingFile.getOriginalName());
                result.put("duplicate", true);
                return result;
            } else if (existingFile != null && !fileExistsInFs) {
                // 数据库有记录但文件系统不存在，说明文件可能被删除了，允许重新上传
                // 清理数据库中的无效记录
                logger.info("检测到数据库中有记录但文件系统不存在，清理无效记录: MD5={}, 原始文件名={}, 数据库ID={}", 
                    fileMd5, existingFile.getOriginalName(), existingFile.getId());
                try {
                    fileInfoMapper.deleteByFileMd5(fileMd5);
                    logger.info("已清理数据库中的无效记录: fileMd5={}", fileMd5);
                } catch (Exception e) {
                    logger.warn("清理数据库无效记录失败: fileMd5={}, error={}", fileMd5, e.getMessage());
                    // 清理失败不影响继续上传
                }
                // 继续上传流程，不返回
            } else if (existingFile == null && fileExistsInFs) {
                // 数据库没有记录但文件系统存在，可能是之前的删除操作没有完全清理
                // 删除文件系统中的文件，然后继续上传
                logger.warn("文件系统中存在文件但数据库无记录，删除文件系统中的文件: {}, MD5: {}", finalFileName, fileMd5);
                try {
                    Files.deleteIfExists(finalFilePath);
                    logger.info("已删除文件系统中已存在的文件: {}", finalFileName);
                } catch (IOException deleteEx) {
                    logger.warn("删除已存在文件失败: {}", finalFilePath, deleteEx);
                    // 删除失败不影响继续上传，但记录警告
                }
                // 继续上传流程，不返回
            }
            // else: 数据库和文件系统都不存在，正常上传流程

            // 检查已上传的分片
            Set<Integer> uploadedChunks = getUploadedChunks(fileMd5);
            result.put("status", "uploading");
            result.put("uploadedChunks", new ArrayList<>(uploadedChunks));
            result.put("totalChunks", totalChunks);
            result.put("fileMd5", fileMd5);
            result.put("finalFileName", finalFileName);

            logger.info("分片状态检查: fileMd5={}, uploaded={}/{}",
                    fileMd5, uploadedChunks.size(), totalChunks);

        } catch (Exception e) {
            logger.error("检查分片状态失败: fileMd5={}", fileMd5, e);
            throw new RuntimeException("检查分片状态失败: " + e.getMessage());
        }

        return result;
    }

    /**
     * 上传分片
     */
    public Map<String, Object> uploadChunk(String fileMd5, String fileName, Integer chunkNumber,
                                           Integer totalChunks, Long chunkSize, Long fileSize,
                                           MultipartFile chunk) {
        Map<String, Object> result = new HashMap<>();

        try {
            // 验证分片大小
            if (chunk.getSize() > maxChunkSize) {
                throw new IOException("分片大小超过限制: " + (maxChunkSize / 1024 / 1024) + "MB");
            }

            // 创建分片目录
            Path chunkDirPath = Paths.get(chunkDir, fileMd5);
            if (!Files.exists(chunkDirPath)) {
                Files.createDirectories(chunkDirPath);
            }

            // 保存分片文件
            String chunkFileName = chunkNumber + ".chunk";
            Path chunkFilePath = chunkDirPath.resolve(chunkFileName);
            Files.copy(chunk.getInputStream(), chunkFilePath, StandardCopyOption.REPLACE_EXISTING);

            // 更新上传进度
            uploadProgress.computeIfAbsent(fileMd5, k -> ConcurrentHashMap.newKeySet())
                    .add(chunkNumber);

            result.put("chunkNumber", chunkNumber);
            result.put("status", "success");
            result.put("message", "分片上传成功");

            logger.info("分片上传成功: fileMd5={}, fileName={}, chunk={}/{}, chunkSize={} bytes",
                    fileMd5, fileName, chunkNumber, totalChunks, chunk.getSize());

        } catch (Exception e) {
            logger.error("分片上传失败: fileMd5={}, chunk={}", fileMd5, chunkNumber, e);
            throw new RuntimeException("分片上传失败: " + e.getMessage());
        }

        return result;
    }

    /**
     * 合并分片
     */
    public Map<String, Object> mergeChunks(String fileMd5, String fileName, Integer totalChunks, String uploadUser, String fileType) {
        Map<String, Object> result = new HashMap<>();

        try {
            // 创建最终文件路径
            String finalFileName = getFinalFileName(fileMd5, fileName);
            Path finalFilePath = Paths.get(uploadDir, finalFileName);
            
            // 检查文件是否真实存在（同时检查数据库和文件系统）
            // 只有当数据库有记录 AND 文件系统也存在文件时，才认为是完整的重复文件，拒绝上传
            // 首次上传时，数据库和文件系统都不存在，这是正常流程
            com.childenglish.entity.FileInfo existingFile = fileInfoMapper.selectByFileMd5(fileMd5);
            boolean fileExistsInFs = Files.exists(finalFilePath);
            
            if (existingFile != null && fileExistsInFs) {
                // 数据库有记录且文件系统也存在
                // 检查是否是同一个上传任务的重复请求（文件刚上传，时间戳很近）
                long currentTime = System.currentTimeMillis();
                long fileUploadTime = existingFile.getUploadTime() != null ? 
                    existingFile.getUploadTime().getTime() : 0;
                long timeDiff = currentTime - fileUploadTime;
                
                // 如果文件是在30秒内上传的，且是同一个用户，可能是重复请求，返回成功
                if (timeDiff < 30000 && uploadUser != null && uploadUser.equals(existingFile.getUploadUser())) {
                    logger.info("检测到重复合并请求（可能是同一上传任务的重复请求），返回成功: MD5={}, 原始文件名={}, 时间差={}ms", 
                        fileMd5, fileName, timeDiff);
                    // 返回已存在的文件信息，表示上传成功
                    result.put("fileName", existingFile.getFileName());
                    result.put("fileSize", existingFile.getFileSize());
                    result.put("status", "success");
                    result.put("message", "文件已存在");
                    return result;
                }
                
                // 否则，说明是真正的重复上传，拒绝
                logger.warn("文件已存在（数据库和文件系统都存在），MD5: {}, 原始文件名: {}, 数据库文件名: {}, 上传时间差: {}ms", 
                    fileMd5, fileName, existingFile.getOriginalName(), timeDiff);
                throw new IOException("文件已存在，不能重复上传。已存在的文件: " + existingFile.getOriginalName());
            } else if (existingFile != null && !fileExistsInFs) {
                // 数据库有记录但文件系统不存在，说明文件可能被删除了，允许重新上传
                // 清理数据库中的无效记录
                logger.info("检测到数据库中有记录但文件系统不存在，清理无效记录: MD5={}, 原始文件名={}, 数据库ID={}", 
                    fileMd5, existingFile.getOriginalName(), existingFile.getId());
                try {
                    fileInfoMapper.deleteByFileMd5(fileMd5);
                    logger.info("已清理数据库中的无效记录: fileMd5={}", fileMd5);
                } catch (Exception e) {
                    logger.warn("清理数据库无效记录失败: fileMd5={}, error={}", fileMd5, e.getMessage());
                    // 清理失败不影响继续上传
                }
                // 继续上传流程（这是首次上传或重新上传）
            } else if (existingFile == null && fileExistsInFs) {
                // 数据库没有记录但文件系统存在，可能是残留文件或之前的删除操作没有完全清理
                // 删除文件系统中的文件，然后继续合并
                logger.warn("文件系统中存在文件但数据库无记录，删除残留文件: {}, MD5: {}", finalFileName, fileMd5);
                try {
                    Files.deleteIfExists(finalFilePath);
                    logger.info("已删除文件系统中残留的文件: {}", finalFileName);
                } catch (IOException deleteEx) {
                    logger.warn("删除残留文件失败: {}", finalFilePath, deleteEx);
                    // 删除失败不影响继续合并，因为可能是其他进程正在写入
                    // 不抛出异常，允许继续合并（合并时会覆盖）
                }
                // 继续上传流程
            }
            // else: 数据库和文件系统都不存在，这是正常的首次上传流程，继续合并

            Path chunkDirPath = Paths.get(chunkDir, fileMd5);
            if (!Files.exists(chunkDirPath)) {
                throw new IOException("分片目录不存在: " + fileMd5);
            }

            // 检查所有分片是否已上传
            Set<Integer> uploadedChunks = getUploadedChunks(fileMd5);
            if (uploadedChunks.size() != totalChunks) {
                throw new IOException("分片不完整: " + uploadedChunks.size() + "/" + totalChunks);
            }
            
            // 在合并前再次检查（防止在检查后、合并前有并发操作）
            // 只有当数据库有记录 AND 文件系统也存在文件时，才认为是真正的重复上传
            com.childenglish.entity.FileInfo preMergeCheck = fileInfoMapper.selectByFileMd5(fileMd5);
            boolean preMergeFileExistsInFs = Files.exists(finalFilePath);
            
            if (preMergeCheck != null && preMergeFileExistsInFs) {
                // 数据库有记录且文件系统也存在
                // 检查是否是同一个上传任务的重复请求（文件刚上传，时间戳很近）
                long currentTime = System.currentTimeMillis();
                long fileUploadTime = preMergeCheck.getUploadTime() != null ? 
                    preMergeCheck.getUploadTime().getTime() : 0;
                long timeDiff = currentTime - fileUploadTime;
                
                // 如果文件是在30秒内上传的，且是同一个用户，可能是重复请求，返回成功
                if (timeDiff < 30000 && uploadUser != null && uploadUser.equals(preMergeCheck.getUploadUser())) {
                    logger.info("合并前检查：检测到重复合并请求（可能是同一上传任务的重复请求），返回成功: MD5={}, 原始文件名={}, 时间差={}ms", 
                        fileMd5, preMergeCheck.getOriginalName(), timeDiff);
                    // 返回已存在的文件信息，表示上传成功
                    result.put("fileName", preMergeCheck.getFileName());
                    result.put("fileSize", preMergeCheck.getFileSize());
                    result.put("status", "success");
                    result.put("message", "文件已存在");
                    return result;
                }
                
                // 否则，说明是真正的重复上传，拒绝
                logger.warn("合并前再次检查发现文件已存在: MD5={}, 原始文件名={}, 数据库ID={}, 上传时间差: {}ms", 
                    fileMd5, preMergeCheck.getOriginalName(), preMergeCheck.getId(), timeDiff);
                throw new IOException("文件已存在，不能重复上传。已存在的文件: " + preMergeCheck.getOriginalName());
            } else if (preMergeCheck != null && !preMergeFileExistsInFs) {
                // 数据库有记录但文件不存在，清理无效记录后继续（允许重新上传）
                logger.info("合并前检查：数据库有记录但文件不存在，清理无效记录: MD5={}", fileMd5);
                try {
                    fileInfoMapper.deleteByFileMd5(fileMd5);
                } catch (Exception e) {
                    logger.warn("清理数据库无效记录失败: fileMd5={}", fileMd5, e);
                }
                // 继续合并流程（这是首次上传或重新上传）
            } else if (preMergeCheck == null && preMergeFileExistsInFs) {
                // 文件系统存在但数据库无记录，可能是残留文件，删除后继续
                // 注意：这不应该在首次上传时发生，因为此时文件还没有合并
                logger.warn("合并前检查：文件系统存在但数据库无记录，删除残留文件: {}", finalFileName);
                try {
                    Files.deleteIfExists(finalFilePath);
                    logger.info("已删除残留文件: {}", finalFileName);
                } catch (IOException deleteEx) {
                    logger.warn("删除残留文件失败: {}", finalFilePath, deleteEx);
                    // 删除失败不影响继续合并，因为可能是其他进程正在写入
                }
                // 继续合并流程
            }
            // else: 数据库和文件系统都不存在，这是正常的首次上传流程，继续合并

            try (RandomAccessFile outputFile = new RandomAccessFile(finalFilePath.toFile(), "rw")) {
                // 按顺序合并分片
                for (int i = 1; i <= totalChunks; i++) {
                    Path chunkFilePath = chunkDirPath.resolve(i + ".chunk");
                    if (!Files.exists(chunkFilePath)) {
                        throw new IOException("分片文件不存在: " + chunkFilePath);
                    }

                    byte[] chunkData = Files.readAllBytes(chunkFilePath);
                    outputFile.write(chunkData);

                    logger.debug("合并分片: {}/{}", i, totalChunks);
                }
            }

            // 清理临时分片文件
            cleanupChunkDir(chunkDirPath);

            // 移除上传进度
            uploadProgress.remove(fileMd5);

            long fileSize = Files.size(finalFilePath);
            
            // 保存文件名映射到元数据文件（用于文件系统扫描时获取原始文件名）
            try {
                fileStorageService.saveFileNameMapping(finalFileName, fileName);
                logger.info("文件名映射已保存: {} -> {}", finalFileName, fileName);
            } catch (Exception e) {
                logger.warn("保存文件名映射失败，但不影响文件合并: {} -> {}, error={}", finalFileName, fileName, e.getMessage());
                // 文件名映射保存失败不影响文件合并，继续执行
            }
            
            // 保存文件信息到数据库，设置审核状态为待审核(0)
            try {
                // 在保存前最后一次检查重复（防止并发上传）
                // 注意：此时文件系统已经存在刚合并的文件，这是正常的首次上传流程
                // 只有当数据库已经有记录时，才认为是真正的重复上传
                com.childenglish.entity.FileInfo duplicateCheck = fileInfoMapper.selectByFileMd5(fileMd5);
                
                if (duplicateCheck != null) {
                    // 数据库已有记录，说明是真正的重复上传（可能是并发上传）
                    // 需要检查文件系统是否也存在（确保是完整的重复文件）
                    boolean duplicateFileExistsInFs = Files.exists(finalFilePath);
                    
                    if (duplicateFileExistsInFs) {
                        // 数据库有记录且文件系统也存在
                        // 检查是否是同一个上传任务的重复请求（文件刚上传，时间戳很近）
                        long currentTime = System.currentTimeMillis();
                        long fileUploadTime = duplicateCheck.getUploadTime() != null ? 
                            duplicateCheck.getUploadTime().getTime() : 0;
                        long timeDiff = currentTime - fileUploadTime;
                        
                        // 如果文件是在30秒内上传的，且是同一个用户，可能是重复请求，返回成功
                        if (timeDiff < 30000 && uploadUser != null && uploadUser.equals(duplicateCheck.getUploadUser())) {
                            logger.info("保存前检查：检测到重复合并请求（可能是同一上传任务的重复请求），返回成功: MD5={}, 原始文件名={}, 时间差={}ms", 
                                fileMd5, duplicateCheck.getOriginalName(), timeDiff);
                            // 删除刚合并的文件（因为已存在），但返回成功
                            try {
                                Files.deleteIfExists(finalFilePath);
                                logger.info("已删除重复合并的文件: {}", finalFilePath);
                            } catch (IOException deleteEx) {
                                logger.warn("删除重复文件失败: {}", finalFilePath, deleteEx);
                            }
                            // 清理分片目录（如果还存在）
                            try {
                                cleanupChunkDir(chunkDirPath);
                            } catch (Exception e) {
                                logger.warn("清理分片目录失败: {}", chunkDirPath, e);
                            }
                            // 返回已存在的文件信息，表示上传成功
                            result.put("fileName", duplicateCheck.getFileName());
                            result.put("fileSize", duplicateCheck.getFileSize());
                            result.put("status", "success");
                            result.put("message", "文件已存在");
                            return result;
                        }
                        
                        // 否则，说明是真正的重复上传，拒绝
                        logger.warn("检测到并发上传，文件已存在（数据库和文件系统都存在）: MD5={}, 原始文件名={}, 数据库ID={}, 上传时间差: {}ms", 
                            fileMd5, duplicateCheck.getOriginalName(), duplicateCheck.getId(), timeDiff);
                        // 删除刚合并的文件，因为已存在
                        try {
                            Files.deleteIfExists(finalFilePath);
                            logger.info("已删除重复合并的文件: {}", finalFilePath);
                        } catch (IOException deleteEx) {
                            logger.warn("删除重复文件失败: {}", finalFilePath, deleteEx);
                        }
                        // 清理分片目录（如果还存在）
                        try {
                            cleanupChunkDir(chunkDirPath);
                        } catch (Exception e) {
                            logger.warn("清理分片目录失败: {}", chunkDirPath, e);
                        }
                        throw new IOException("文件已存在，不能重复上传。已存在的文件: " + duplicateCheck.getOriginalName());
                    } else {
                        // 数据库有记录但文件系统不存在，说明文件可能被删除了，清理无效记录后继续
                        logger.info("检测到数据库中有记录但文件系统不存在，清理无效记录后继续: MD5={}, 原始文件名={}, 数据库ID={}", 
                            fileMd5, duplicateCheck.getOriginalName(), duplicateCheck.getId());
                        try {
                            fileInfoMapper.deleteByFileMd5(fileMd5);
                            logger.info("已清理数据库中的无效记录: fileMd5={}", fileMd5);
                        } catch (Exception e) {
                            logger.warn("清理数据库无效记录失败: fileMd5={}, error={}", fileMd5, e.getMessage());
                            // 清理失败不影响继续保存
                        }
                        // 继续保存文件信息（这是首次上传）
                    }
                } else {
                    // 数据库没有记录，这是正常的首次上传流程
                    // 文件系统存在是正常的（刚合并的文件），继续保存
                    logger.debug("首次上传，保存文件信息到数据库: fileMd5={}, fileName={}", fileMd5, finalFileName);
                    // 继续保存文件信息
                }
                
                // 确定文件类型
                String determinedFileType = fileType != null ? fileType : getFileTypeFromName(fileName);
                logger.info("准备保存文件信息到数据库: fileMd5={}, fileName={}, originalName={}, fileType={}, fileSize={}, totalChunks={}, uploadUser={}", 
                    fileMd5, finalFileName, fileName, determinedFileType, fileSize, totalChunks, uploadUser);
                
                com.childenglish.entity.FileInfo fileInfo = new com.childenglish.entity.FileInfo();
                fileInfo.setFileMd5(fileMd5);
                fileInfo.setFileName(finalFileName);
                fileInfo.setOriginalName(fileName);
                fileInfo.setFilePath(finalFilePath.toString());
                fileInfo.setFileSize(fileSize);
                fileInfo.setFileType(determinedFileType);
                fileInfo.setChunked(1);
                fileInfo.setTotalChunks(totalChunks);
                // 计算分片大小（平均分片大小）
                if (totalChunks > 0) {
                    fileInfo.setChunkSize(fileSize / totalChunks);
                } else {
                    fileInfo.setChunkSize(fileSize);
                }
                fileInfo.setStatus(2); // 合并完成
                fileInfo.setUploadUser(uploadUser);
                fileInfo.setReviewStatus(0); // 待审核
                fileInfo.setUploadTime(new java.util.Date());
                fileInfo.setCompleteTime(new java.util.Date());
                
                // 尝试获取MIME类型
                try {
                    String mimeType = Files.probeContentType(finalFilePath);
                    fileInfo.setMimeType(mimeType != null ? mimeType : "application/octet-stream");
                } catch (Exception e) {
                    logger.warn("获取MIME类型失败，使用默认值: {}", e.getMessage());
                    fileInfo.setMimeType("application/octet-stream");
                }
                
                // 打印所有字段值用于调试
                logger.debug("文件信息对象详情: {}", fileInfo.toString());
                
                int insertResult = fileInfoMapper.insert(fileInfo);
                logger.info("数据库插入操作结果: insertResult={}, 生成的主键ID={}", insertResult, fileInfo.getId());
                
                // 无论insertResult如何，都验证数据库是否真的有记录
                com.childenglish.entity.FileInfo verifyFile = fileInfoMapper.selectByFileMd5(fileMd5);
                if (verifyFile == null) {
                    // 数据库中没有记录，说明插入失败
                    logger.error("❌ 文件信息保存失败: insertResult={}, 但数据库中没有找到该文件, fileMd5={}, fileName={}", 
                        insertResult, fileMd5, finalFileName);
                    throw new RuntimeException("文件信息保存到数据库失败: 数据库中没有找到该文件记录");
                } else {
                    // 验证成功，记录日志
                    logger.info("✅ 文件信息已成功保存到数据库: id={}, fileName={}, originalName={}, fileType={}, reviewStatus=0(待审核), uploadUser={}", 
                        verifyFile.getId(), finalFileName, fileName, verifyFile.getFileType(), verifyFile.getUploadUser());
                    // 如果insertResult为0但数据库中有记录，可能是并发插入
                    if (insertResult == 0) {
                        logger.warn("⚠️ insert返回0，但数据库中已存在该文件，可能是并发插入: id={}", verifyFile.getId());
                    }
                }
            } catch (IOException e) {
                // 如果是重复上传的错误，重新抛出
                throw e;
            } catch (Exception e) {
                logger.error("❌ 保存文件信息到数据库失败: fileMd5={}, fileName={}, error={}", fileMd5, finalFileName, e.getMessage(), e);
                // 如果数据库保存失败，删除已合并的文件和文件名映射
                try {
                    Files.deleteIfExists(finalFilePath);
                    logger.warn("由于数据库保存失败，已删除合并的文件: {}", finalFilePath);
                    // 注意：文件名映射会在文件删除时自动清理，这里不需要手动删除
                } catch (IOException deleteEx) {
                    logger.error("删除文件失败: {}", finalFilePath, deleteEx);
                }
                // 抛出异常，确保前端能收到错误信息
                throw new RuntimeException("文件信息保存到数据库失败: " + e.getMessage(), e);
            }

            result.put("fileName", finalFileName);
            result.put("fileSize", fileSize);
            result.put("status", "success");
            result.put("message", "文件合并成功");

            logger.info("文件合并成功: fileMd5={}, originalFileName={}, finalFileName={}, filePath={}, fileSize={} bytes", 
                fileMd5, fileName, finalFileName, finalFilePath.toAbsolutePath(), fileSize);

        } catch (Exception e) {
            logger.error("文件合并失败: fileMd5={}", fileMd5, e);
            throw new RuntimeException("文件合并失败: " + e.getMessage());
        }

        return result;
    }

    /**
     * 根据文件名获取文件类型
     */
    private String getFileTypeFromName(String fileName) {
        String extension = fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase();
        
        // 课件类型
        if (Arrays.asList("pdf", "ppt", "pptx", "doc", "docx", "txt").contains(extension)) {
            return "courseware";
        } 
        // 绘本类型（图片）
        else if (Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp").contains(extension)) {
            return "picturebook";
        }
        // 音频类型
        else if (Arrays.asList("mp3", "wav", "ogg", "flac", "aac", "m4a", "wma", "mp4").contains(extension)) {
            return "audio";
        }
        
        return "other";
    }

    /**
     * 支持断点续传的下载
     */
    public ResponseEntity<Resource> downloadWithRange(String fileName, String rangeHeader) {
        try {
            Path filePath = Paths.get(uploadDir, fileName);
            if (!Files.exists(filePath)) {
                return ResponseEntity.notFound().build();
            }

            long fileSize = Files.size(filePath);
            long rangeStart = 0;
            long rangeEnd = fileSize - 1;

            // 解析Range头
            if (rangeHeader != null) {
                String[] ranges = rangeHeader.substring("bytes=".length()).split("-");
                rangeStart = Long.parseLong(ranges[0]);
                if (ranges.length > 1) {
                    rangeEnd = Long.parseLong(ranges[1]);
                }
            }

            long contentLength = rangeEnd - rangeStart + 1;

            // 读取文件片段
            byte[] fileData;
            try (RandomAccessFile file = new RandomAccessFile(filePath.toFile(), "r")) {
                file.seek(rangeStart);
                fileData = new byte[(int) contentLength];
                int bytesRead = file.read(fileData);
                
                logger.info("读取文件片段: fileName={}, range={}-{}, 期望大小={}, 实际读取={}", 
                        fileName, rangeStart, rangeEnd, contentLength, bytesRead);
                
                if (bytesRead < contentLength) {
                    // 如果读取的字节数少于期望值，调整数组大小
                    byte[] actualData = new byte[bytesRead];
                    System.arraycopy(fileData, 0, actualData, 0, bytesRead);
                    fileData = actualData;
                    logger.warn("文件片段读取不完整: fileName={}, 期望={}, 实际={}", 
                            fileName, contentLength, bytesRead);
                }
                
                if (fileData.length == 0) {
                    logger.error("读取的文件片段为空: fileName={}, range={}-{}", fileName, rangeStart, rangeEnd);
                    throw new IOException("读取的文件片段为空");
                }
            }

            ByteArrayResource resource = new ByteArrayResource(fileData);

            HttpHeaders headers = new HttpHeaders();
            headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"");
            headers.add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_OCTET_STREAM_VALUE);
            headers.add(HttpHeaders.ACCEPT_RANGES, "bytes");
            headers.add(HttpHeaders.CONTENT_RANGE, "bytes " + rangeStart + "-" + rangeEnd + "/" + fileSize);
            headers.add(HttpHeaders.CONTENT_LENGTH, String.valueOf(contentLength));

            HttpStatus status = HttpStatus.PARTIAL_CONTENT;
            if (rangeStart == 0 && rangeEnd == fileSize - 1) {
                status = HttpStatus.OK;
            }

            return ResponseEntity.status(status)
                    .headers(headers)
                    .body(resource);

        } catch (Exception e) {
            logger.error("断点续传下载失败: fileName={}", fileName, e);
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
    }

    /**
     * 获取下载信息
     */
    public Map<String, Object> getDownloadInfo(String fileName) {
        Map<String, Object> info = new HashMap<>();

        try {
            Path filePath = Paths.get(uploadDir, fileName);
            if (!Files.exists(filePath)) {
                throw new IOException("文件不存在: " + fileName);
            }

            info.put("fileName", fileName);
            info.put("fileSize", Files.size(filePath));
            info.put("lastModified", Files.getLastModifiedTime(filePath).toMillis());
            info.put("supportRange", true);
            info.put("contentType", Files.probeContentType(filePath));

        } catch (Exception e) {
            logger.error("获取下载信息失败: fileName={}", fileName, e);
            throw new RuntimeException("获取下载信息失败: " + e.getMessage());
        }

        return info;
    }

    /**
     * 清理临时分片文件
     */
    public boolean cleanupChunks(String fileMd5) {
        try {
            Path chunkDirPath = Paths.get(chunkDir, fileMd5);
            if (Files.exists(chunkDirPath)) {
                cleanupChunkDir(chunkDirPath);
                uploadProgress.remove(fileMd5);
                logger.info("清理临时分片文件: fileMd5={}", fileMd5);
                return true;
            }
            return false;
        } catch (Exception e) {
            logger.error("清理临时分片文件失败: fileMd5={}", fileMd5, e);
            return false;
        }
    }

    /**
     * 获取已上传的分片列表
     */
    private Set<Integer> getUploadedChunks(String fileMd5) {
        Set<Integer> uploadedChunks = new TreeSet<>();

        try {
            Path chunkDirPath = Paths.get(chunkDir, fileMd5);
            if (Files.exists(chunkDirPath)) {
                Files.list(chunkDirPath)
                        .filter(path -> path.toString().endsWith(".chunk"))
                        .forEach(path -> {
                            try {
                                String fileName = path.getFileName().toString();
                                int chunkNumber = Integer.parseInt(fileName.substring(0, fileName.indexOf('.')));
                                uploadedChunks.add(chunkNumber);
                            } catch (NumberFormatException e) {
                                logger.warn("无效的分片文件名: {}", path.getFileName());
                            }
                        });
            }
        } catch (IOException e) {
            logger.error("获取已上传分片列表失败: fileMd5={}", fileMd5, e);
        }

        return uploadedChunks;
    }

    /**
     * 清理分片目录
     */
    private void cleanupChunkDir(Path chunkDirPath) throws IOException {
        if (Files.exists(chunkDirPath)) {
            Files.walk(chunkDirPath)
                    .sorted(Comparator.reverseOrder())
                    .map(Path::toFile)
                    .forEach(File::delete);
        }
    }

    /**
     * 生成最终文件名
     */
    private String getFinalFileName(String fileMd5, String originalFileName) {
        String extension = "";
        if (originalFileName.contains(".")) {
            extension = originalFileName.substring(originalFileName.lastIndexOf("."));
        }
        return fileMd5 + extension;
    }
}